Codable保姆级攻略

Codable保姆级攻略

前语

Codable 是随 Swift 4.0 推出的,旨在取代现有的 NSCoding 协议,支持结构体、枚举和类,能够将 JSON 这种弱数据类型转化成代码中运用的强数据类型。

Codable 是一个协议,同时统筹了 Decodable 和 Encodable 两个协议,假如你界说了一个数据类型遵照了 Codable 协议,其实是遵照了 Decodable 和 Encodable:

typealias Codable = Decodable & Encodable

说白了便是一套转化协议,能够让数据和 Swift 中的数据类型依据某种映射关系进行转化,比方 JSON 转成你 Swift 中自界说的数据类型。

Codable保姆级攻略

HandyJSON 在 Codable 推出后已经不再进行维护了,而咱们的项目便是依赖于 HandyJSON 来处理 JSON 的序列化和反序列化,所以咱们也需求逐渐迁移到 Codable 以防止一些过错。

运用技巧

1.基本运用

struct Person: Codable {
  let name: String
  let age: Int
}
// 解码
let json = #" {"name":"Tom", "age": 2} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)
print(person) //Person(name: "Tom", age: 2
// 编码
let data0 = try? JSONEncoder().encode(person) 
let dataObject = try? JSONSerialization.jsonObject(with: data0!, options: []) 
print(dataObject ?? "nil") //{ age = 2; name = Tom; }
let data1 = try? JSONSerialization.data(withJSONObject: ["name": person.name, "age": person.age], options: []) 
print(String(data: data1!, encoding: .utf8)!) //{"name":"Tom","age":2}

2.字段映射

struct Person: Codable {
  let name: String
  let age: Int
  let countryName: String
  private enum CodingKeys: String, CodingKey {
    case countryName = "country"
    case name
    case age
  }
}
let json = #" {"name":"Tom", "age": 2, "country": "China"} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)
print(person) //Person(name: "Tom", age: 2, countryName: "China")

在 Swift 4.1 后,JSONDecoder 增加了 keyDecodingStrategy 特点,假如后端运用带下划线的,蛇形命名法,经过将 keyDecodingStrategy 特点的值设置为 convertFromSnakeCase,这样就不需求写额外的代码来处理映射了:

var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// 后端 person_name
// 映射为 personName

3.解析枚举值

解析枚举值有个坑便是原本比方你只要三个 case

enum TestEnum: String {
	case a
	case b
	case c
}

然后服务端某天多加了几个你没有处理的 case,比方加了个 case d,那这样去解析就直接崩掉了,所以咱们需求想办法给不知道的 case 设置一个默认值。 具体的完成是这样,增加一个协议:

protocol CodableEnumeration: RawRepresentable, Codable where RawValue: Codable {
  static var defaultCase: Self { get }
}
extension CodableEnumeration {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    do {
      let decoded = try container.decode(RawValue.self)
      self = Self.init(rawValue: decoded) ?? Self.defaultCase
    } catch {
      self = Self.defaultCase
    }
  }
}

然后在遇到不知道的 case 时就能够经过让 Enum 遵守 CodableEnumeration ,然后完成 defaultCase 办法来指定一个 case 即可。

/**
{
	"test_enum" : "d"
}
*/
enum TestEnum: String, CodableEnumeration {
  static var defaultCase: TestEnum {
    .a
  }
  case a
  case b
  case c
}
struct Test: Codable {
  var testEnum: TestEnum
  private enum CodingKeys: String, CodingKey {
    case testEnum = "test_enum"
  }
}
let testJson = #" {"test_enum":"d"} "#
let test = try JSONDecoder().decode(Test.self, from: testJson.data(using: .utf8)!)
print(test.testEnum) // a

4.解析嵌套类型

Swift4 支持条件一致性,所以当数组中的每个元素都遵照 Codable 协议,字典中对应的 keyvalue 遵照 Codable 协议,全体目标就遵照 Codable 协议,便是确保你嵌套的类型都遵照 Codable 协议即可。

struct Student: Codable {
  var id: String
  var name: String
  var grade: Int
}
struct Class: Codable {
  var classNumber: String
  var students: [Student]
}
/**
{
	"classNumber": "111",
	"students": [{
		"id": "1",
		"name": "studentA",
		"grade": 1
	}, {
		"id": "2",
		"name": "studentB",
		"grade": 2
	}]
}
*/
let classJson = #"{"classNumber":"111","students":[{"id":"1","name":"studentA","grade":1},{"id":"2","name":"studentB","grade":2}]}"#
let classModel = try JSONDecoder().decode(Class.self, from: classJson.data(using: .utf8)!)
print(classModel)
/** 输出
▿ Class
  - classNumber : "111"
  ▿ students : 2 elements
    ▿ 0 : Student
      - id : "1"
      - name : "studentA"
      - grade : 1
    ▿ 1 : Student
      - id : "2"
      - name : "studentB"
      - grade : 2
*/

5.解析空值,为null时指定默认值

后端的接口回来的数据可能有值也可能是为空的,这个时候能够将特点设置为可选类型:

/**
{
	"name": "xiaoming",
	"age": 18,
	"partner": null
}
*/
struct Person: Codable {
  let name: String
  let age: Int
  let partner: String?
}
let personJson = #"{"name":"xiaoming","age":18,"partner":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person) // Person(name: "xiaoming", age: 18, partner: nil)

假如不想运用可选类型,能够重写 init(from decoder: Decoder) throws 办法,来指定一个默认的值:

struct Person: Codable {
  var name: String
  var age: Int
  var partner: String
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    age = try container.decode(Int.self, forKey: .age)
    name = try container.decode(String.self, forKey: .name)
    partner = try container.decodeIfPresent(String.self, forKey: .partner) ?? ""
  }
}
let personJson = #"{"name":"xiaoming","age":18,"partner":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person) // Person(name: "xiaoming", age: 18, partner: "")

现在只要三个特点,假如特点很多,那写起来就非常费事,好在咱们有更好的计划: Annotating properties with default decoding values 简略来说便是经过 @propertyWrapper 来进行优化,在需求默认值的特点上对这个特点进行声明,编译器就能够主动帮助咱们完成赋默认值的操作,当然,这个特点包裹的完成要咱们自己去完成,代码我就直接贴出来,新建个类放到项目中即可:

protocol DecodableDefaultSource {
  associatedtype Value: Decodable
  static var defaultValue: Value { get }
} 
enum DecodableDefault {}
extension DecodableDefault {
  @propertyWrapper
  struct Wrapper<Source: DecodableDefaultSource> {
    typealias Value = Source.Value
    var wrappedValue = Source.defaultValue
  }
}
extension DecodableDefault.Wrapper: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    wrappedValue = try container.decode(Value.self)
  }
}
extension KeyedDecodingContainer {
  func decode<T>(_ type: DecodableDefault.Wrapper<T>.Type,
         forKey key: Key) throws -> DecodableDefault.Wrapper<T> {
    try decodeIfPresent(type, forKey: key) ?? .init()
  }
}
extension DecodableDefault {
  typealias Source = DecodableDefaultSource
  typealias List = Decodable & ExpressibleByArrayLiteral
  typealias Map = Decodable & ExpressibleByDictionaryLiteral
  enum Sources {
    enum True: Source { static var defaultValue: Bool { true } }
    enum False: Source { static var defaultValue: Bool { false } }
    enum EmptyString: Source { static var defaultValue: String { "" } }
    enum EmptyList<T: List>: Source { static var defaultValue: T { [] } }
    enum EmptyMap<T: Map>: Source { static var defaultValue: T { [:] } }
    enum Zero: Source { static var defaultValue: Int { 0 } }
  }
} 
extension DecodableDefault {
  typealias True = Wrapper<Sources.True>
  typealias False = Wrapper<Sources.False>
  typealias EmptyString = Wrapper<Sources.EmptyString>
  typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>>
  typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>>
  typealias Zero = Wrapper<Sources.Zero>
}
extension DecodableDefault.Wrapper: Equatable where Value: Equatable {}
extension DecodableDefault.Wrapper: Hashable where Value: Hashable {}
extension DecodableDefault.Wrapper: Encodable where Value: Encodable {
  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(wrappedValue)
  }
}

假如需求增加更多的默认值,在 Sources 中增加即可,上面的 Zero 便是额外加的,能够验证一下:

/**
{
	"name":null,
	"money":null,
	"skills":null,
	"teachers":null
}
*/
struct Person: Codable {
  @DecodableDefault.EmptyString var name: String
  @DecodableDefault.Zero var money: Int
  @DecodableDefault.EmptyList var skills: [String]
  @DecodableDefault.EmptyList var teachers: [Teacher]
} 
struct Teacher: Codable {
  @DecodableDefault.EmptyString var name: String
} 
let personJson = #"{"name":null,"money":null,"skills":null,"teachers":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person)
/** 输出
▿ Person
 ▿ _name : Wrapper<EmptyString>
  - wrappedValue : ""
 ▿ _money : Wrapper<Zero>
  - wrappedValue : 0
 ▿ _skills : Wrapper<EmptyList<Array<String>>>
  - wrappedValue : 0 elements
 ▿ _teachers : Wrapper<EmptyList<Array<Teacher>>>
  - wrappedValue : 0 elements
*/

6.编码

取 5 中的 person,对它进行编码,并输出字符串或转成对应的数据格式:

// 字符串
do {
  let jsonData = try JSONEncoder().encode(person)
  let jsonString = String(decoding: jsonData, as: UTF8.self)
  print(jsonString) // {"money":0,"teachers":[],"name":"","skills":[]}
} catch {
  print(error.localizedDescription)
}
// 字典
do {
  let jsonData = try JSONEncoder().encode(person)
  let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String : Any]
  print(jsonObject ?? "null")
} catch {
  print(error.localizedDescription)
}

小结

大约常用的操作便是这些,运用起来仍是很方便的,HandyJSON 是依赖于 Swift 的 Runtime 源码揣度内存规矩,假如规矩改变,那么 HandyJSON 就不管用了,而 HandyJSON 现在也不再进行维护,所以在规矩没有改变之前,咱们需求逐渐弃用 HandyJSON 改用原生的 Codable。

参阅

Annotating properties with default decoding values

Encoding and Decoding Custom Types